/**
 * Model package.
 */
package fr.umlv.m2.ig.model;

import java.awt.Point;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import fr.umlv.m2.ig.handler.XMLMapHandler;

/**
 * Defines Beauheur model.
 * 
 * @author Gambouele Bertrand.
 * @author Viravout Alexandre.
 */
public class BeauheurModelImpl extends AbstractBeauheurModel {

	/**
	 * The default station waiting time.
	 */
	public static final Double TIME = 2.0;

	/**
	 * The default speed of train.
	 */
	public static final Double TRAIN_SPEED = 10.0;

	/**
	 * Map of lines.
	 */
	private final Map<String, Line> lines;

	/**
	 * Map of stations.
	 */
	private final SortedMap<String, Station> stations;

	/**
	 * Map of trains.
	 */
	private final Map<String, Train> trains;

	/**
	 * The map width.
	 */
	private final int mapWidth;

	/**
	 * The map height.
	 */
	private final int mapHeight;

	/**
	 * Refresh time.
	 */
	private final int refresh;

	/**
	 * A boolean to prevent model from changing while saving state.
	 */
	private boolean isLocked;

	@Override
	public int getMapWidth() {
		return this.mapWidth;
	}

	@Override
	public int getMapHeight() {
		return this.mapHeight;
	}

	/**
	 * Creates a new {@code BeauheurModelImpl} object.
	 * 
	 * @throws SAXException if an error occurs while parsing the map XML file.
	 * @throws IOException if an I/O error occurs while reading map XML file.
	 */
	public BeauheurModelImpl(final int refresh) throws SAXException, IOException {
		XMLMapHandler handler = parseMapFile();
		this.refresh = refresh;
		this.mapWidth = handler.getMapWidth();
		this.mapHeight = handler.getMapHeight();
		this.stations = handler.getStations();
		this.lines = handler.getLines();
		this.trains = new HashMap<String, Train>();
		Iterator<Entry<String, Line>> lines = this.lines.entrySet().iterator();
		Iterator<Entry<String, Station>> stations = this.stations.entrySet().iterator();

		Line line;
		while(lines.hasNext()) {
			line = lines.next().getValue();
			line.setSpeed(TRAIN_SPEED);
		}

		Station station;
		while(stations.hasNext()) {
			station = stations.next().getValue();
			for(String l : station.getLines()) {
				if(station.getType() == StationType.STATION) {
					station.setWaitingTime(l, TIME);
				} else if(station.getType() == StationType.CURVE) {
					station.setWaitingTime(l, 0.0);
				}
			}
		}
	}

	/**
	 * Parses the XML file and fill the model with objects.
	 * 
	 * @return A n handler containing retrieved objects from the parsed file.
	 * @throws SAXException if an exception occurs while parsing.
	 * @throws IOException if an I/O exception occurs while parsing.
	 */
	private XMLMapHandler parseMapFile() throws SAXException, IOException {
		XMLMapHandler handler = new XMLMapHandler();
		XMLReader reader = XMLReaderFactory.createXMLReader();
		reader.setContentHandler(handler);

		InputStream mapFile = this.getClass().getResourceAsStream("/map.xml");
		InputSource source = new InputSource(mapFile);

		try {
			reader.parse(source);
		} finally {
			mapFile.close();
		}

		return handler;
	}

	@Override
	public Station getStation(String stationName) {
		return stations.get(stationName);
	}

	@Override
	public Map<String, Station> getStations() {
		return Collections.unmodifiableSortedMap(this.stations);
	}

	@Override
	public Map<String, Line> getLines() {
		return Collections.unmodifiableMap(this.lines);
	}

	/**
	 * Fire the view to display stations by line.
	 * 
	 * @param line the stations line.
	 */
	public void searchDisplayStationsByLine(String line) {
		this.fireSearchDisplayByLine(line);
	}

	/**
	 * Fire the view to display stations according a name.
	 * 
	 * @param name the name to match with the station.
	 */
	public void searchDisplayStationsByName(String name) {
		this.fireSearchDisplayByName(name);
	}

	/**
	 * Fire the train move.
	 */
	public void moveTrains() {
		if (this.isLocked) {
			return;
		}

		List<Train> movedTrains = new ArrayList<Train>();

		for (Train train : trains.values()) {
			Line line = this.lines.get(train.getLine());
			Station nextStation = this.stations.get(train.getNextStation());

			if (!nextStation.isBusy(line.getName(), train.getDestination())) {
				double lastPositionX = train.getPositionX();
				double lastPositionY = train.getPositionY();

				boolean moved = train.move();

				if (moved) {
					Station currentStation = this.stations.get(train.getCurrentStation());
					Point p = currentStation.getCoordinates(line.getName());

					if (lastPositionX == p.x && lastPositionY == p.y) {
						currentStation.setBusy(line.getName(), train.getDestination(), false);
					}

					if (train.getRemainingMoves() == 0) {
						currentStation = nextStation;

						if (!train.getDestination().equals(currentStation.getName())) {
							currentStation.setBusy(line.getName(), train.getDestination(), true);
						}

						train.setCurrentStation(currentStation.getName());

						if (currentStation.getType() == StationType.STATION && currentStation.getState(line.getName()) == StationState.OPENED) {
							train.setWaitingTime((int) ((currentStation.getWaitingTime(line.getName()) * 1000.0) / refresh));
						} else {
							train.setWaitingTime(0);
						}

						if (currentStation.getName().equals(train.getDestination())) {
							train.reverseItinerary();
							currentStation.setBusy(line.getName(), train.getDestination(), true);
						}

						p = currentStation.getCoordinates(line.getName());

						train.setPosition(p.getX(), p.getY());

						String nextStationName = currentStation.getNextStation(train.getLine(), train.getDestination());

						nextStation = this.stations.get(nextStationName);
						train.setNextStation(nextStationName);

						Point q = null;

						if (train != null){
							q = nextStation.getCoordinates(train.getLine());
						}

						double speed = line.getSpeed() * (refresh/1000.0);

						double distanceX = q.getX() - p.getX();
						double distanceY = q.getY() - p.getY();

						double distanceAbs = Math.max(Math.abs(distanceX), Math.abs(distanceY));
						int nbMoves = (int) Math.round(distanceAbs / speed);

						if (nbMoves == 0) {
							nbMoves = 1;
						}

						train.setRemainingMoves(nbMoves);

						double stepX = distanceX / nbMoves;
						double stepY = distanceY / nbMoves;

						train.setStepX(stepX);
						train.setStepY(stepY);
					}

					movedTrains.add(train);
				}
			}
		}

		if (!movedTrains.isEmpty()) {
			fireTrainsMoved(movedTrains);
		}
	}

	/**
	 * Change station state.
	 * 
	 * @param stationName the name of the station to changed.
	 * @param line the line of the station to changed.
	 */
	public void changeStationState(String stationName, String line) {
		if (this.isLocked) {
			return;
		}

		Station station = this.stations.get(stationName);

		if (station != null) {
			station.changeState(line);
			fireStateChanged(station);
		}
	}

	/**
	 * Adds a new train in the model.
	 * 
	 * @param train train to be added.
	 */
	public void addTrain(Train train) {
		if (this.isLocked) {
			return;
		}

		if (train != null) {
			Station currentStation = this.stations.get(train.getCurrentStation());
			boolean freeStation = true;

			if (currentStation.isBusy(train.getLine(), train.getDestination())) {
				freeStation = false;
			}

			if (freeStation) {
				String station1 = train.getCurrentStation();
				String dest = train.getDestination();

				List<Train> trainsList = new ArrayList<Train>(this.trains.values());

				for (int i = 0; freeStation && i < trainsList.size(); i++) {
					Train tmpTrain = trainsList.get(i);

					if (station1.equals(tmpTrain.getCurrentStation()) && dest.equals(tmpTrain.getDestination())) {
						freeStation = false;
					} else if (station1.equals(tmpTrain.getNextStation()) && station1.equals(tmpTrain.getDestination())) {
						freeStation = false;
					}
				}
			}

			if (freeStation) {
				currentStation.setBusy(train.getLine(), train.getDestination(), true);
				Line line = this.lines.get(train.getLine());
				Point p = currentStation.getCoordinates(line.getName());

				train.setPosition(p.getX(), p.getY());

				String nextStationName = currentStation.getNextStation(train.getLine(), train.getDestination());
				Station nextStation = this.stations.get(nextStationName);

				train.setNextStation(nextStationName);

				double speed = line.getSpeed() * (refresh/1000.0);
				Point q = nextStation.getCoordinates(line.getName());

				double distanceX = q.getX() - p.getX();
				double distanceY = q.getY() - p.getY();

				double distanceAbs = Math.max(Math.abs(distanceX), Math.abs(distanceY));
				int nbMoves = (int) Math.round(distanceAbs / speed);

				if (nbMoves == 0) {
					nbMoves = 1;
				}

				train.setRemainingMoves(nbMoves);

				if (currentStation.getState(line.getName()) == StationState.OPENED) {
					train.setWaitingTime((int) ((currentStation.getWaitingTime(line.getName()) * 1000.0) / refresh));
				} else {
					train.setWaitingTime(0);
				}

				double stepX = distanceX / nbMoves;
				double stepY = distanceY / nbMoves;

				train.setStepX(stepX);
				train.setStepY(stepY);

				this.trains.put(train.getId(), train);
				fireTrainAdded(train);
			}
		}
	}

	/**
	 * Removes the specified train from the model.
	 * 
	 * @param train train to be removed.
	 */
	public void removeTrain(Train train) {
		if (this.isLocked) {
			return;
		}

		if (train != null) {
			Train removed = this.trains.remove(train.getId());

			if (removed != null) {
				Station currentStation = this.stations.get(train.getCurrentStation());
				Point p = currentStation.getCoordinates(train.getLine());

				if (train.getPositionX() == p.x && train.getPositionY() == p.y) {
					currentStation.setBusy(train.getLine(), train.getDestination(), false);
				}

				fireTrainRemoved(removed);
			}
		}
	}

	/**
	 * Sets the station waiting time.
	 * 
	 * @param stationName the station to set.
	 * @param time the time to wait.
	 */
	public void setWaitingTime(String stationName, String lineName, Double time) {
		Station station = stations.get(stationName);
		station.setWaitingTime(lineName, time);
	}

	/**
	 * Gets the station waiting time for a line.
	 * 
	 * @param stationName the station.
	 * @param lineName the line name.
	 * @return the waiting time.
	 */
	public Double getWaitingTime(String stationName, String lineName) {
		Station station = stations.get(stationName);
		return station.getWaitingTime(lineName);
	}

	/**
	 * Sets the train speed.
	 * 
	 * @param lineName the line to apply the speed.
	 * @param speed the trains speed.
	 */
	public void setTrainSpeed(String lineName, Double speed) {
		Line line = lines.get(lineName);
		line.setSpeed(speed);
	}

	/**
	 * Gets the train speed for a line.
	 * 
	 * @param lineName the line.
	 */
	public Double getTrainSpeed(String lineName) {
		Line line = lines.get(lineName);
		return line.getSpeed();
	}

	/**
	 * Locks the model to prevent from moving trains.
	 * Use this method while saving the model state.
	 */
	public void lock() {
		this.isLocked = true;
	}

	/**
	 * Unlocks the model.
	 */
	public void unlock() {
		this.isLocked = false;
	}

	/**
	 * Returns the list of trains.
	 * 
	 * @return list of trains.
	 */
	public List<Train> getTrains() {
		return Collections.unmodifiableList(new ArrayList<Train>(trains.values()));
	}

	/**
	 * Adds the specified train in the model.
	 * 
	 * @param train the train to be added.
	 */
	public void addTrainFromConfigFile(Train train) {
		if (train != null) {
			this.trains.put(train.getId(), train);
			fireTrainAdded(train);
		}
	}

	/**
	 * Sets the next station for the specified train.
	 * 
	 * @param train a train.
	 * @param nextStation the next station name.
	 */
	public void setTrainNextStation(Train train, String nextStation) {
		if (train != null) {
			train.setNextStation(nextStation);
		}
	}

	/**
	 * Sets location for the specified train.
	 * 
	 * @param train a train.
	 * @param positionX X coordinate.
	 * @param positionY Y coordinate.
	 */
	public void setTrainPosition(Train train, double positionX, double positionY) {
		if (train != null) {
			train.setPosition(positionX, positionY);
		}
	}

	/**
	 * Sets waiting time for the specified train.
	 * 
	 * @param train a train.
	 * @param waitingTime waiting time.
	 */
	public void setTrainWaitingTime(Train train, int waitingTime) {
		if (train != null) {
			train.setWaitingTime(waitingTime);
		}
	}

	/**
	 * Sets the number of remaining moves for this train.
	 * 
	 * @param train a train.
	 * @param remainingMoves Number of remaining moves.
	 */
	public void setTrainRemainingMoves(Train train, int remainingMoves) {
		if (train != null) {
			train.setRemainingMoves(remainingMoves);
		}
	}

	/**
	 * Sets the X move step for the specified train.
	 * 
	 * @param train a train.
	 * @param stepX X move step.
	 */
	public void setTrainMoveStepX(Train train, double stepX) {
		if (train != null) {
			train.setStepX(stepX);
		}
	}

	/**
	 * Sets the Y move step for the specified train.
	 * 
	 * @param train a train.
	 * @param stepY Y move step.
	 */
	public void setTrainMoveStepY(Train train, double stepY) {
		if (train != null) {
			train.setStepY(stepY);
		}
	}

	/**
	 * Removes all trains from the model.
	 */
	public void resetTrainsList() {
		this.trains.clear();
		fireTrainsListReseted();
	}

	/**
	 * Sets the station state to the specified value.
	 * 
	 * @param stationName station name.
	 * @param line station line.
	 * @param state station state.
	 */
	public void setStationState(String stationName, String line, StationState state) {
		Station station = this.stations.get(stationName);

		if (station != null) {
			if (station.getState(line) != state) {
				station.changeState(line);
			}
			
			fireStateChanged(station);
		}
	}

	/**
	 * Sets if the specified station is busy or not.
	 * 
	 * @param stationName the concerned station.
	 * @param line line name.
	 * @param destination concerned destination.
	 * @param busy a boolean indicating if the station is busy.
	 */
	public void setStationBusy(String stationName, String line, String destination, boolean busy) {
		Station station = this.stations.get(stationName);

		if (station != null) {
			station.setBusy(line, destination, busy);
		}
	}
}
